package com.ece.core.config;

import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;

import org.apache.xml.serialize.OutputFormat;
import org.apache.xml.serialize.XMLSerializer;
import org.w3c.dom.Document;
import org.xml.sax.SAXException;

/**
 * XML based config parser. Gets some of its inputs from the exntentions to the
 * provided extension point <code>com.ece.core.xmlconfigadapters</code>
 * 
 * @author Senthu Sivasambu, http://ca.linkedin.com/in/sivasambu
 * 
 */
public class XMLConfigParser extends AbstractConfigParser {

    static final String JAXP_SCHEMA_LANGUAGE = "http://java.sun.com/xml/jaxp/properties/schemaLanguage";

    static final String W3C_XML_SCHEMA = "http://www.w3.org/2001/XMLSchema";

    static final String JAXP_SCHEMA_SOURCE = "http://java.sun.com/xml/jaxp/properties/schemaSource";

    private final File schemaSource;

    private final Map<String, org.w3c.dom.Document> cache = new HashMap<String, org.w3c.dom.Document>();

    private boolean refresh = false;

    /**
     * This is made private for the following reasons - at the time the plugins
     * are initiated we may not have the inputs required to call this
     * constructor<br/>
     * - in the future we may need to create different flavors of instances of
     * this class
     * 
     * @param parseSourceName
     *            - a String reference to the target parse source. In this will
     *            be XML file. However we still do not know the which file -
     *            meaning at which level of the repo.
     * @param schemaSource
     *            - {@link File} reference to the schema location as provided by
     *            the extension points.
     */
    private XMLConfigParser(File schemaSource, String parseSourceName) {
	super(parseSourceName);
	this.schemaSource = schemaSource;
    }

    public static XMLConfigParser createXMLParser(IConfigAdapter adapter) {
	return new XMLConfigParser(new File(adapter.getSchemaLocation()),
		(adapter.getFileName() + adapter.getFileExtension()));
    }

    /**
     * This interface is required to parse XML files on the fly. The file names
     * cannot be provided via extensions declaratively, rather they must be
     * supplied programaticaly as and when required.
     * 
     * @param parseSourceFileName
     *            - file name must be with the extension.
     * @param parentDirectory
     * @param callback
     * @return
     */
    public org.w3c.dom.Document parse(String parseSourceFileName,
	    File parentDirectory, IParserCallback callback) {
	setParseSourceName(parseSourceFileName);
	return parse(parentDirectory, callback);
    }

    /**
     * @param parentDirectory
     *            - the directory structure in which the parser should look for
     *            the target parse file
     * @return
     */
    public org.w3c.dom.Document parse(File parentDirectory,
	    IParserCallback callback) {

	if (!parentDirectory.isDirectory()) {
	    throw new IllegalArgumentException(
		    "provided argument is not a directory");
	}

	Document tmp = cache.get(parentDirectory.getPath());
	if (!refresh) {
	    if (tmp != null) {
		return tmp;
	    }
	} else {
	    cache.remove(parentDirectory.getPath());
	}

	refresh = false;

	DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
	factory.setNamespaceAware(true);
	factory.setValidating(true);
	File parseSource = new File(parentDirectory.getPath()
		+ File.pathSeparator + getParseSourceName());

	try {

	    if (!parseSource.exists()) {
		Logger.getAnonymousLogger().log(Level.INFO,
			"File does not exist, creating a new file...");
		/* creates a news file with a initial content */
		DocumentBuilder docBuilder = factory.newDocumentBuilder();
		InputStream in = new ByteArrayInputStream(callback
			.getInitialFileContent().getBytes());

		Document document = docBuilder.parse(in);
		serialize(parentDirectory, document);
		return document;
	    }

	    factory.setAttribute(JAXP_SCHEMA_LANGUAGE, W3C_XML_SCHEMA);
	    factory.setAttribute(JAXP_SCHEMA_SOURCE, getParseSourceName());
	    DocumentBuilder documentBuilder = factory.newDocumentBuilder();
	    Document document = documentBuilder.parse(parseSource);
	    cache.put(parentDirectory.getPath(), document);
	    return document;

	} catch (IllegalArgumentException iae) {
	    // if no support JAXP 1.2
	    Logger.getAnonymousLogger()
		    .log(Level.SEVERE,
			    "Configuring the document builder to supposer JAXP 1.2 failed");
	    Logger.getAnonymousLogger().log(Level.SEVERE, iae.getMessage());
	} catch (ParserConfigurationException pce) {
	    Logger.getAnonymousLogger().log(Level.SEVERE, pce.getMessage());
	} catch (SAXException se) {
	    Logger.getAnonymousLogger().log(Level.SEVERE, se.getMessage());
	    se.printStackTrace();
	} catch (IOException ioe) {
	    Logger.getAnonymousLogger().log(Level.SEVERE, ioe.getMessage());
	    ioe.printStackTrace();
	}

	return null;
    }

    public boolean persist(File parentDirectory) {
	if (parentDirectory == null || !parentDirectory.isDirectory()) {
	    throw new IllegalArgumentException(
		    "@parem parentDirecotry is either null or not a directory");
	}

	Document document = cache.get(parentDirectory.getPath());

	serialize(parentDirectory, document);
	return false;
    }

    private boolean serialize(File parentDirectory, Document document) {
	if (document != null) {

	    @SuppressWarnings("deprecation")
	    OutputFormat outFormat = new OutputFormat(document);
	    outFormat.setIndenting(true);
	    File writeTarget = new File((parentDirectory.getPath()
		    + File.pathSeparator + getParseSourceName()));
	    try {

		OutputStream out = new FileOutputStream(writeTarget);
		XMLSerializer seralizer = new XMLSerializer(out, outFormat);
		seralizer.serialize(document);
		return true;

	    } catch (FileNotFoundException fnfe) {
		Logger.getAnonymousLogger().log(Level.SEVERE,
			"could not locate the write target");
	    } catch (IOException ioe) {
		Logger.getAnonymousLogger()
			.log(Level.SEVERE,
				"something went wrong while writing to the target file...");
		ioe.printStackTrace();
	    }
	    XMLSerializer serializer = new XMLSerializer();
	    return true;
	} else {
	    Logger.getAnonymousLogger().log(Level.INFO,
		    "No parse document is found in the cache to persist");
	    return false;
	}
    }

    public void refresh() {
	refresh = true;
    }
}
